﻿/*
Pyramid Dependency Manager
Copyright (c) 2011, Joel Wenzel
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * The names of the Pyramid Dependency Manager's contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL Joel Wenzel BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

*/
var Pyramid = new (function () {


	//only works for string and ints and not for objects
	function __UniqueArray() {
	    var _vals = new Array();
	    var _indices = new Array();
	
	    _vals.add = function (val) {
	        if (_vals.contains(val))
	            return;
	
	        _indices[val] = _vals.length;
	        _vals[_vals.length] = val;
	    };
	
	    _vals.remove = function (val) {
	        _vals.splice(_indices[val], 1);
	        _indices[val] = undefined;
	    };
	
	    _vals.contains = function (val) {
	        return _indices[val] != undefined;
	    };
	
	    return _vals;
	}
	
	//the purpose of this object is to ensure that the keys can always be retrieved in the order they are added
	//not all implimentations of associative arrays are traversed in the order the items are added
	function __OrderedHashArray() {
	    var _vals = new Array();
	    _vals.keys = new __UniqueArray();
	
	    _vals.set = function (key, val) {
	        _vals[key] = val;
	
	        if (!_vals.keys.contains(key))
	            _vals.keys.add(key);
	    };
	
	    _vals.containsKey = function (key) {
	        return _vals.keys.contains(key);
	    };
	
	    _vals.size = function () {
	        return _vals.keys.length;
	    };
	
	    return _vals;
	}


    //=======================================
    // Utility Functions
    //=======================================

    getExtension = function (filename) {
        return /[^.]+$/.exec(filename)
    };

    function getUrlText(file) {
        var xmlhttp;
        if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari
            xmlhttp = new XMLHttpRequest();
        }
        else {// code for IE6, IE5
            xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        }

        xmlhttp.open("GET", file, false);
        xmlhttp.send();
        return xmlhttp.responseText;
    }

    function indexOf(needle, arr) {
        for (var i = 0; i < arr.length; i++) {
            if (arr[i] == needle) {
                return i;
            }
        }
        return -1;
    }
    
	//=======================================
    // Renderer - writes a file to a webpage according to a template
    //=======================================
    __Renderer = function (rootPath) {
        var thatDepManRend = this;
        this.template = undefined;

        this.getRenderedString = function (file) {
            if (thatDepManRend.template)
                return (new String(thatDepManRend.template)).replace('${file}', file);
            return getUrlText(file);
        };
    };

    //=======================================
    // TypeRenderers - tracks all the various renderers
    //=======================================
    __TypeRenderers = function () {
        var _renderers = new __OrderedHashArray();

        this.get = function (name) { var lookupName = (new String(name)).toLowerCase(); return _renderers[lookupName]; };

        this.set = function (name, isExtension, template) {
            var lookupName = (new String(name)).toLowerCase();
            if (!_renderers.containsKey(lookupName))
                _renderers.set(lookupName, new __Renderer());
            _renderers[lookupName].template = template
        };
    };

    //=======================================
    // Dependency
    //=======================================
    __Dependency = function (name) {
        this.name = name;
        var thatDepManDep = this;
        var _files = new __OrderedHashArray();
        var _dependencyNames = new __UniqueArray();

        File = function (fname, rendererLookUp) {
            this.uniqueHashKey = (new String(fname)).toLowerCase();
            this.filePath = fname;
            this.rendererLookUp = rendererLookUp;
        }

        this.addDependency = function (dependencyName) {
            _dependencyNames.add(dependencyName);
        };

        this.addFiles = function (filesToAdd, typeRendererName) {
            var renderToUse = typeRendererName;
            for (var i = 0; i < filesToAdd.length; i++) {
                if (!typeRendererName)
                    renderToUse = getExtension(filesToAdd[i]);

                _files.set(new String(filesToAdd[i]).toLowerCase(),
                new File(filesToAdd[i], renderToUse));
            }
        };



        this.__getFiles = function (dupsArray, noRecursion) {
            var filesArray = new Array();

            //noRecursion is false, or undefined
            if (!noRecursion) {
                for (var i = 0; i < _dependencyNames.length; i++) {
                    var dependency = _dependenciesMasterList[_dependencyNames[i]];
                    filesArray = filesArray.concat(dependency.__getFiles(dupsArray));
                }
            }

            for (var i = 0; i < _files.keys.length; i++) {
                var file = _files[_files.keys[i]];
                if (!dupsArray[file.uniqueHashKey]) {
                    filesArray.push(file.filePath);
                    dupsArray[file.uniqueHashKey] = true;
                }
            }

            return filesArray;
        }

        this.getFiles = function (previouslyLoadedFiles, noRecursion) {
            if (!previouslyLoadedFiles)
                previouslyLoadedFiles = new Array();
            return thatDepManDep.__getFiles(previouslyLoadedFiles, noRecursion);
        };

        this.__render = function (dupsArray, rootPath, noRecursion) {
            var html = '';
            //noRecursion is false, or undefined
            if (!noRecursion) {
                for (var i = 0; i < _dependencyNames.length; i++) {
                    var dependency = _dependenciesMasterList[_dependencyNames[i]];
                    html += dependency.__render(dupsArray, rootPath);
                }
            }

            for (var i = 0; i < _files.keys.length; i++) {
                var file = _files[_files.keys[i]];
                if (!dupsArray[file.uniqueHashKey]) {

                    var rendererLookUp = file.rendererLookUp;
                    var renderer = _typeRenderers.get(rendererLookUp);
                    if (!renderer)
                        throw 'Missing required type renderer for file "' + file.filePath + '"';

                    html += renderer.getRenderedString(rootPath + file.filePath);
                    dupsArray[file.uniqueHashKey] = true;
                }
            }

            return html;
        }

        this.render = function (rootPath, previouslyLoadedFiles, noRecursion) {
            if (!previouslyLoadedFiles)
                previouslyLoadedFiles = new Array();

            var html = thatDepManDep.__render(previouslyLoadedFiles, rootPath, noRecursion);
            thatDepMan.insertContentMethod(html);
        };
    };

    //=======================================
    // Pyramid
    //=======================================
    var thatDepMan = this;
    
    var _typeRenderers = new __TypeRenderers();
    var _dependenciesMasterList = new __OrderedHashArray();
    var _previouslyLoadedFiles = new Array();

	//base reference path for the dependencies
	this.rootPath = "/";
	
	//method which determines where the dependencies are included in the file
	this.insertContentMethod = function (content) { document.write(content); };

    
	
	// Determines how a file is rendered for inclusion in a webpage
	// options.name - name of the renderer
	// options.isExtension - determines if the name is an extension
	// options.template - template used to render a file for inclusion.  Template uses the variable ${file} to
	// represent the path to the file to be used for rendering.
    this.setTypeRenderer = function (options) {
        if (options.name == undefined)
            throw 'options.name is a required parameter';

        var isExtension = true;
        if (options.isExtension)
            isExtension = options.isExtension;

        _typeRenderers.set(options.name, isExtension, options.template);
        return thatDepMan;
    };

	//returns an array of all the files required for a particular dependencyGroup.  Note that parents are recursed so files 
	// for parent groups are returned as well.
    this.getFiles = function (dependencyGroup) {
        var dep = _dependenciesMasterList[dependencyGroup];
        if (!dep)
            throw 'No dependency group:' + dependencyGroup;
        return dep.getFiles();
    };

    /*
    options.name: dependency group name (required)
    options.files: array of file paths relative to root 
    options.dependencies: array of dependency group names
    options.filesRenderer: render used to insert files
    */
    this.createOrUpdate = function (options) {

        if (!options.name || (!options.files && !options.dependencies))
            throw 'create/createOrUpdate requires options.name and (options.files and/or options.dependencies) as parameters';

        if (!_dependenciesMasterList.containsKey(options.name))
            _dependenciesMasterList.set(options.name, new __Dependency(options.name, _dependenciesMasterList, _typeRenderers));

        var newDep = _dependenciesMasterList[options.name];

        //add dependency groups
        if (options.dependencies) {
            for (var i = 0; i < options.dependencies.length; i++) {

                var prevDependency = _dependenciesMasterList[options.dependencies[i]];
                if (!prevDependency)
                    throw 'Dependency "' + options.dependencies[i] + '" is not defined';

                newDep.addDependency(options.dependencies[i]);
            }
        }

        //add files
        if (options.files) {
            newDep.addFiles(options.files, options.filesRenderer);
        }

        //allow chaining
        return thatDepMan;
    };

    //throws an error if group already exists
    this.create = function (options) {
        if (_dependenciesMasterList.containsKey(options.name))
            throw 'Cannot create dependency "' + options.name + '" as it already exists';

        return thatDepMan.createOrUpdate(options);
    };

    /*
	joins files into a single file for each renderer.  For example, joins js files and css files separately
	
    options.name: dependency group name to combine
	options.include: files to include (instead of using options.name)
    options.excludeDependencies: list of files to exclude
    options.excludeFiles: list of files to exclude
    */
    this.joinFiles = function (options) {

        var files = [];

        //populate the files array
        if (options.name) {
            files = thatDepMan.getFiles(options.name);
            for (var i = 0; i < files.length; i++) {
                files[i] = thatDepMan.rootPath + files[i];
            }
        }

        if (options.include) {
            for (var i = 0; i < options.include.length; i++) {
                if (indexOf(options.include[i], files) == -1)
                    files.push(options.include[i]);
            }
        }

        if (options.excludeFiles) {
            for (var i = 0; i < options.excludeFiles.length; i++) {
                var index = indexOf(options.excludeFiles[i], files);
                if (index > -1)
                    files.splice(index, 1);
            }
        }

        if (options.excludeDependencies) {
            for (var i = 0; i < options.excludeDependencies.length; i++) {
                var depName = options.excludeDependencies[i];
                var excludeFiles = thatDepMan.getFiles(depName, true);

                for (var j = 0; j < excludeFiles.length; j++) {

                    var index = indexOf(thatDepMan.rootPath + excludeFiles[j], files);
                    if (index > -1)
                        files.splice(index, 1);
                }
            }
        }

        //join files
        var aText = [];
        for (var i = 0; i < files.length; i++) {
            var path = files[i];
            var ex = getExtension(path);
            if (!aText[ex])
                aText[ex] = '';

            aText[ex] += '\n\n' + getUrlText(path);
        }

        return aText;
    };

	//gets include string of all renderers combined
	//eg <script...> ...
	//Params:
	// depName - Name of dependencies group
    this.getIncludeString = function (depName) {
        if (!_dependenciesMasterList[depName]) {
            throw 'Could not load dependency "' + depName + '" as it has not been added as a dependency';
        }

        return _dependenciesMasterList[depName].__render(thatDepMan.rootPath, _previouslyLoadedFiles);
    };

	//loads the dependencies passed as arguments
	//Params:
	//  each dependency group name separated by commas
    this.load = function () {
        for (var i = 0; i < arguments.length; i++) {
            var depName = arguments[i];
            if (!_dependenciesMasterList[depName]) {
                throw 'Could not load dependency "' + depName + '" as it has not been added as a dependency';
            }

            _dependenciesMasterList[depName].render(thatDepMan.rootPath, _previouslyLoadedFiles);
        }
        return thatDepMan;
    };
})();

Pyramid.setTypeRenderer({
	name: 'js',
	isExtension: true,
	template: '<script type="text/javascript" src="${file}"></script>\n'
	});

Pyramid.setTypeRenderer({
	name: 'css',
	isExtension: true,
	template: '<link rel="stylesheet" href="${file}" type="text/css"/>\n'
	});

Pyramid.setTypeRenderer({
	name: '',
	isExtension: true
	});